﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Application.Controls
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;

    /// <summary>Found on <a href="http://msdn.microsoft.com/en-us/library/ms229658(VS.100).aspx">MSDN</a>. Copyright
    /// 2009 Microsoft Corporation.</summary>
    internal delegate int WndProcCallback(IntPtr hwnd, uint msg, uint wParam, int lParam, ref bool handled);

    /// <summary>Modified version of the original found on
    /// <a href="http://msdn.microsoft.com/en-us/library/ms229658(VS.100).aspx">MSDN</a>. Original is
    /// Copyright 2009 Microsoft Corporation. Modifications are copyrighted as indicated in the header of this file.
    /// </summary>
    internal static class WndProcHooker
    {
        // This is the global list of all the window procedures we have
        // hooked. The key is an hwnd. The value is a HookedProcInformation
        // object which contains a pointer to the old wndproc and a map of
        // message's callbacks for the window specified. Controls whose handles
        // have been created go into this dictionary.
        private static readonly Dictionary<IntPtr, HookedProcInformation> hwndDict =
            new Dictionary<IntPtr, HookedProcInformation>();

        // The key for this dictionary is a control and the value is a
        // HookedProcInformation. Controls whose handles have not been created
        // go into this dictionary. When the HandleCreated event for the
        // control is fired the control is moved into hwndDict.
        private static readonly Dictionary<Control, HookedProcInformation> ctlDict =
            new Dictionary<Control, HookedProcInformation>();

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        // Makes a connection between a message on a specified window handle
        // and the callback to be called when that message is received. If the
        // window was not previously hooked it is added to the global list of
        // all the window procedures hooked.
        // Parameters:
        // ctl - The control whose wndproc we are hooking.
        // callback - The method to call when the specified.
        // message is received for the specified window.
        // msg - The message being hooked.
        internal static void HookWndProc(Control ctl, WndProcCallback callback, uint msg)
        {
            HookedProcInformation hpi = null;

            if (ctlDict.ContainsKey(ctl))
            {
                hpi = ctlDict[ctl];
            }
            else if (hwndDict.ContainsKey(ctl.Handle))
            {
                hpi = hwndDict[ctl.Handle];
            }

            if (hpi == null)
            {
                // If new control, create a new
                // HookedProcInformation for it.
                hpi = new HookedProcInformation(ctl, WndProcHooker.WindowProc);
                ctl.HandleCreated += OnHandleCreated;
                ctl.HandleDestroyed += OnHandleDestroyed;
                ctl.Disposed += OnControlDisposed;

                // If the handle has already been created set the hook. If it
                // hasn't been created yet, the hook will get set in the
                // ctl_HandleCreated event handler.
                if (ctl.Handle != IntPtr.Zero)
                {
                    hpi.SetHook();
                }
            }

            // Stick hpi into the correct dictionary.
            if (ctl.Handle == IntPtr.Zero)
            {
                ctlDict[ctl] = hpi;
            }
            else
            {
                hwndDict[ctl.Handle] = hpi;
            }

            // Add the message/callback into the message map.
            hpi.MessageMap[msg] = callback;
        }

        // Restores the previous wndproc for the specified window.
        // Parameters:
        // ctl - The control whose wndproc we no longer want to hook.
        // disposing - True if HookedProcInformation is not
        //   read back into ctlDict.
        internal static void UnhookWndProc(Control ctl, bool disposing)
        {
            HookedProcInformation hpi = null;

            if (ctlDict.ContainsKey(ctl))
            {
                hpi = ctlDict[ctl];
            }
            else if (hwndDict.ContainsKey(ctl.Handle))
            {
                hpi = hwndDict[ctl.Handle];
            }

            if (hpi == null)
            {
                throw new ArgumentException("No hook exists for this control");
            }

            // If we found our HookedProcInformation in ctlDict and we are
            // disposing remove it from ctlDict.
            if (ctlDict.ContainsKey(ctl) && disposing)
            {
                ctlDict.Remove(ctl);
            }

            // If we found our HookedProcInformation in hwndDict, remove it
            // and if we are not disposing stick it in ctlDict.
            if (hwndDict.ContainsKey(ctl.Handle))
            {
                hpi.Unhook();
                hwndDict.Remove(ctl.Handle);

                if (!disposing)
                {
                    ctlDict[ctl] = hpi;
                }
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        // The event handler called when a control is disposed.
        private static void OnControlDisposed(object sender, EventArgs e)
        {
            Control ctl = sender as Control;

            if (ctlDict.ContainsKey(ctl))
            {
                ctlDict.Remove(ctl);
            }
            else
            {
                Debug.Assert(false, "Disposed control has never been hooked.");
            }
        }

        // The event handler called when a control's handle is destroyed.
        // We remove the HookedProcInformation from hwndDict and
        // put it back into ctlDict in case the control get re-
        // created and we still want to hook its messages.
        private static void OnHandleDestroyed(object sender, EventArgs e)
        {
            // When the handle for a control is destroyed, we want to
            // unhook its wndproc and update our lists
            Control ctl = sender as Control;

            if (hwndDict.ContainsKey(ctl.Handle))
            {
                HookedProcInformation hpi = hwndDict[ctl.Handle];
                UnhookWndProc(ctl, false);
            }
            else
            {
                Debug.Assert(false, "Creation of destroyed handle has never been observed.");
            }
        }

        // The event handler called when a control's handle is created. We
        // call SetHook() on the associated HookedProcInformation object and
        // move it from ctlDict to hwndDict.
        private static void OnHandleCreated(object sender, EventArgs e)
        {
            Control ctl = sender as Control;

            if (ctlDict.ContainsKey(ctl))
            {
                HookedProcInformation hpi = ctlDict[ctl];
                hwndDict[ctl.Handle] = hpi;
                ctlDict.Remove(ctl);
                hpi.SetHook();
            }
            else
            {
                Debug.Assert(false, "A handled has been created for an unhooked control.");
            }
        }

        // This is a generic wndproc. It is the callback for all hooked
        // windows. If we get into this function, we look up the hwnd in the
        // global list of all hooked windows to get its message map. If the
        // message received is present in the message map, its callback is
        // invoked with the parameters listed here.
        // Parameters:
        // hwnd - The handle to the window that received the
        // message
        // msg - The message
        // wParam - The message's parameters (part 1)
        // lParam - The messages's parameters (part 2)
        // Returns the callback handled the message, the callback's return
        // value is returned form this function. If the callback didn't handle
        // the message, the message is forwarded on to the previous wndproc.
        [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "wParam is an official name")]
        private static int WindowProc(IntPtr hwnd, uint msg, uint wParam, int lParam)
        {
            if (hwndDict.ContainsKey(hwnd))
            {
                HookedProcInformation hpi = hwndDict[hwnd];

                if (hpi.MessageMap.ContainsKey(msg))
                {
                    WndProcCallback callback = hpi.MessageMap[msg];
                    bool handled = false;
                    int retval = callback(hwnd, msg, wParam, lParam, ref handled);

                    if (handled)
                    {
                        return retval;
                    }
                }

                // If the callback didn't set the handled property to true,
                // call the original window procedure.
                return hpi.CallOldWindowProc(hwnd, msg, wParam, lParam);
            }

            Debug.Assert(false, "WindowProc called for hwnd we don't know about");
            return NativeMethods.DefWindowProc(hwnd, msg, wParam, lParam);
        }

        /// <summary>This class remembers the old window procedure for the specified window handle and also provides the
        /// message map for the messages hooked on that window.</summary>
        [SuppressMessage("Microsoft.Design", "CA1049:TypesThatOwnNativeResourcesShouldBeDisposable", Justification = "The unmanaged resource is owned by the control, so we must not release it.")]
        private sealed class HookedProcInformation
        {
            // Control whose wndproc is being hooked.
            private readonly Control control;

            // The delegate that gets called in place of this window's
            // wndproc.
            private readonly NativeMethods.WndProc newWndProc;

            // The message map for the window.
            private readonly Dictionary<uint, WndProcCallback> messageMap;

            // The old window procedure for the window.
            private IntPtr oldWndProc;

            // Constructs a new HookedProcInformation object
            // Parameters:
            // ctl - The handle to the window being hooked
            // wndproc - The window procedure to replace the
            // original one for the control.
            internal HookedProcInformation(Control ctl, NativeMethods.WndProc wndproc)
            {
                this.control = ctl;
                this.newWndProc = wndproc;
                this.messageMap = new Dictionary<uint, WndProcCallback>();
            }

            internal Dictionary<uint, WndProcCallback> MessageMap
            {
                get { return this.messageMap; }
            }

            // Replaces the windows procedure for control with the
            // one specified in the constructor.
            internal void SetHook()
            {
                IntPtr hwnd = this.control.Handle;

                if (hwnd == IntPtr.Zero)
                {
                    throw new InvalidOperationException("Handle for control has not been created");
                }

                this.oldWndProc = NativeMethods.SetWindowLong(
                    hwnd, NativeMethods.GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(this.newWndProc));
            }

            // Restores the original window procedure for the control.
            internal void Unhook()
            {
                IntPtr hwnd = this.control.Handle;

                if (hwnd == IntPtr.Zero)
                {
                    throw new InvalidOperationException("Handle for control has not been created");
                }

                NativeMethods.SetWindowLong(hwnd, NativeMethods.GWL_WNDPROC, this.oldWndProc);
            }

            // Calls the original window procedure of the control with the
            // arguments provided.
            // Parameters:
            // hwnd - The handle of the window that received the
            // message
            // msg - The message
            // wParam - The message's arguments (part 1)
            // lParam - The message's arguments (part 2)
            // Returns the value returned by the control's original wndproc.
            [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "wParam is an official name")]
            internal int CallOldWindowProc(IntPtr hwnd, uint msg, uint wParam, int lParam)
            {
                return NativeMethods.CallWindowProc(this.oldWndProc, hwnd, msg, wParam, lParam);
            }
        }
    }
}
